1 module hip.api.net.hipnet;
2 public import hip.api.net.server;
3 
4 
5 /**
6  * Network module is one which can't be abstracted much.
7  * That happens because, by using templates, it is possible to reduce the
8  * memory footprint required for sending and getting data.
9  * The implementation of sending data without templates would likely require to allocate memory
10  * on heap instead of stack.
11  *
12  * So, most functionality will actually be implemented on API instead of inside the engine module.
13  *
14  */
15 
16 
17 enum NetConnectStatus
18 {
19 	///Represents basically a blank state where it can transition to connected or waiting
20 	disconnected,
21 	///It is currently connected and getting data will actually send the data instead of trying to connect.
22 	connected,
23 	///It is attempting to connect to the specified IP+ID Address
24 	waiting,
25 	///It was connected, but for some reason, client sent a disconnect message and now it is trying to reconnect
26 	attemptingReconnect
27 }
28 
29 
30 enum IPType : ubyte
31 {
32 	ipv4,
33 	ipv6
34 }
35 
36 struct NetIPAddress
37 {
38 	string ip;
39 	ushort port;
40 	IPType type = IPType.ipv4;
41 }
42 
43 /**
44  * Currently, tcp is used everywhere and websockets are used on WASM
45  */
46 enum NetInterface
47 {
48 	automatic,
49     tcp,
50     websocket
51 }
52 
53 /**
54  * Those are the NetIDs that your server must implement.
55  * Basically, 0 is ought to be a message that the server must interpret
56  * While 1 is the broadcast one. The IDs are mostly relevant to WebSockets
57  */
58 enum NetID : uint
59 {
60 	server,
61 	broadcast,
62 	start,
63 	end = uint.max
64 }
65 
66 /**
67  * Currently, only binary seems to be relevant.
68  * Custom + might be reserved for implementing known formats (such as JSON)
69  */
70 package enum NetDataType : ubyte
71 {
72 	binary,
73 	text,
74 	custom
75 }
76 
77 
78 /**
79  * Usage of Alignment(1) can reduce the memory footprint.
80  * It also should use a fixed size type since there will be no surprises when running
81  * the code via another platform or something like that.
82  */
83 struct NetHeader
84 {
85 	align(1):
86 	uint length;
87 	NetDataType type;
88 
89 	bool invalid() const { return length == 0; }
90 }
91 
92 struct NetConnectInfo
93 {
94     NetIPAddress ip;
95     uint id;
96 }
97 
98 template NetBinding(Req, Resp)
99 {
100 	alias Request = Req;
101 	alias Response = Resp;
102 }
103 
104 
105 struct NetBuffer
106 {
107 	///The header may change after some time since it will be reused.
108 	NetHeader header;
109 	///Buffer may be bigger than expected size as it will be reused
110 	ubyte[] buffer;
111 
112 	size_t expectedSize() const {return header.length; }
113 	bool isInvalid() const { return header.invalid; }
114 
115 	///Gets the buffer only if it has already finished, and sliced to its expected size
116 	ubyte[] getFinishedBuffer()
117 	{
118 		if(expectedSize == 0)
119 			return null;
120 		return buffer[0..expectedSize];
121 	}
122 }
123 
124 /**
125  * Those are the reserved type IDs that are found in every MarkNetData instance.
126  * Since a new instance of this enum is created, the reserved types are written
127  * snake-cased.
128  * Custom type members will have the exact same name as the type they intend to use.
129  *
130  *	connect: void send_connect(INetwork)
131  *	disconnect: void send_disconnect(INetwork)
132  *	get_connected_clients: send_get_connected_clients(INetwork)
133  *  client_connect: send_client_connect(INetwork, uint targetID)
134  */
135 enum MarkedNetReservedTypes : ubyte
136 {
137 	invalid,
138 	///Send that message so NetController can identify that a network connection was established.
139 	@NetBinding!(void, void) connect,
140 	///Send that message so NetController can identify that a network interface was disconnected
141 	@NetBinding!(void, void) disconnect,
142 	///Sends a message to the server requesting for the available connection IDs
143 	@NetBinding!(void, ConnectedClientsResponse) get_connected_clients,
144 	///The ID to connect to. Must be a valid ID received from get_connected_clients
145 	@NetBinding!(uint, ConnectToClientResponse) client_connect
146 }
147 
148 template Attributes(T, string mem)
149 {
150 	alias Attributes = __traits(getAttributes, __traits(getMember, T, mem));
151 }
152 
153 static foreach(m; __traits(allMembers, MarkedNetReservedTypes))
154 {
155 	static if(Attributes!(MarkedNetReservedTypes, m).length)
156 	{
157 		static if(is(Attributes!(MarkedNetReservedTypes, m)[0].Request == void))
158 		{
159 			mixin("void send_",m,"(INetwork net){",
160 			"net.sendDataToServer(__traits(getMember, MarkedNetReservedTypes, m));}");
161 		}
162 		else
163 		{
164 			mixin("void send_",m,"(INetwork net, Attributes!(MarkedNetReservedTypes, m)[0].Request d){",
165 			"pragma(LDC_no_typeinfo) static struct Data { align(1): MarkedNetReservedTypes t; Attributes!(MarkedNetReservedTypes, m)[0].Request data;} ",
166 			"net.sendDataToServer(Data(__traits(getMember, MarkedNetReservedTypes, m), d));}");
167 		}
168 	}
169 }
170 
171 
172 /**
173  * This function is only used inside MarkNetData, this allows to create automatically an enum which is used
174  * for being able to iterate type safely through its members on NetController.
175  *
176  * Params:
177  *   enumName = Enum name that will be generated
178  *   enumType = The enum type
179  * Returns: A string to be mixed which defines a new enum.
180  */
181 private string enumFromTypes(T...)(string enumName, string enumType)
182 {
183 	string ret = "enum "~enumName~": "~enumType~"{";
184 	foreach(mem; __traits(allMembers, MarkedNetReservedTypes))
185 		ret~= mem ~ ", ";
186 	static foreach(t; T)
187 	{
188 		static assert(is(t == struct) || is(t == enum), "MarkNetData only works for enums and structs.");
189 		ret~=  t.stringof~",";
190 	}
191 	return ret~"}";
192 }
193 
194 
195 /**
196  * Creates a set of types which
197  * Params:
198  *   T = The types that are valid to send on network.
199  */
200 template MarkNetData(T...)
201 {
202 	enum PredefinedTypesCount = __traits(allMembers, MarkedNetReservedTypes).length;
203 
204 	static if(T.length <= ubyte.max)
205 		alias idType = ubyte;
206 	else static if(T.length <= ushort.max)
207 		alias idType = ushort;
208 
209 	mixin(enumFromTypes!(T)("Types", "idType"));
210 
211 	string getTypeName(idType v)
212 	{
213 		static foreach(t; T)
214 		{
215 			if(v == __traits(getMember, Types, t.stringof))
216 				return t.stringof;
217 		}
218 		return "Unknown";
219 	}
220 
221     bool isInvalid(idType v)
222     {
223         return v == 0 || v >= T.length;
224     }
225 
226 	static foreach(i, t; T)
227 	{
228 		void sendData(INetwork net, t data)
229 		{
230 			align(1)
231 			static struct TypedData
232 			{
233 				t actualData;
234 				idType typeID = i + PredefinedTypesCount;
235 			}
236 			net.sendData(TypedData(data, i+PredefinedTypesCount));
237 		}
238 	}
239 
240 	/**
241 	 * To its data removing the type id.
242 	 * Params:
243 	 *   buffer = The buffer that is used to take the type id and slices it to the actual data
244 	 * Returns: The type id from that buffer. Also enforces it is not invalid
245 	 */
246 	Types getDataFromBuffer(ref ubyte[] buffer)
247 	{
248 		idType typeID = *cast(idType*)(buffer.ptr + buffer.length - idType.sizeof);
249 		buffer = buffer[0..buffer.length - idType.sizeof];
250 		return cast(Types)typeID;
251 	}
252 
253 
254 
255     bool isUnknown(Q)()
256     {
257         static foreach(t; T)
258 			static if(is(t == Q))
259             	return false;
260         return true;
261     }
262 
263 	alias RegisteredTypes = T;
264 }
265 
266 
267 /**
268  * Implementation currently follows at `hip.network`
269  */
270 interface INetwork
271 {
272 	/**
273 	 *
274 	 * Params:
275 	 *   ip = The IP to connect
276 	 *   onConnect = Delegate to execute when connecting
277 	 *   id = ID of the address to connect to. Relevant when you're not using a P2P connection. Enforced when using websockets, since direct connection is unavailable.
278 	 * Returns: The connection status
279 	 */
280 	NetConnectStatus connect(NetIPAddress ip, void delegate(INetwork) onConnect, uint id = NetID.server);
281 	///Gets ID for that network connection. It can't change over its lifetime
282 	uint getConnectionSelfID() const;
283 	///Sets that network connection connected to the specified ID
284 	void targetConnectionID(uint id);
285 	uint targetConnectionID() const;
286 	bool isHost() const;
287     ///Returns whether it has got any data
288 	NetConnectStatus status() const;
289     NetInterface getNetInterfaceType() const;
290     NetConnectInfo getConnectInfo() const;
291     ///Returns if there is any data available to take
292 	bool getData();
293     ///Sends the data without modifying it further. It is called like that since it will be able to overload with templates
294 	void sendDataRaw(ubyte[] data);
295 
296 	/**
297 	 * WARNING: It is important to call freeBuffer at some time since getting the buffer won't remove it from the queue
298 	 * This function must only be called after getData() returns true.
299 	 * Returns: A buffer stream or [a datagram buffer](This one is only on future)
300 	 */
301 	NetBuffer* getCompletedBuffer() const;
302 	/**
303 	 * Make that buffer available inside the buffer pool and saves memory
304 	 * Params:
305 	 *   buffer = A buffer inside the completed list
306 	 */
307 	void freeBuffer(NetBuffer* buffer);
308 
309     /**
310      * Due to performance reasons, toNetworkBytes and toBytes needs to be in api instead of back implementation.
311      * This allows to use non-allocating memory when sending data.
312      * Keep in mind that the data is converted to big endian before being sent.
313      * Params:
314      *   data = Any data type wanted to send
315      */
316     void sendData(T)(T data)
317 	{
318 		import std.traits:isDynamicArray;
319         import hip.api.net.utils;
320 		uint size = getSendTypeSize(data);
321 
322 		static if(is(T == string))
323 		{
324 			NetHeader header = NetHeader(size, NetDataType.text);
325 		}
326 		else
327 		{
328 			static if(isDynamicArray!T)
329 			{
330 				NetHeader header = NetHeader(size, NetDataType.binary);
331 			}
332 			else
333 			{
334 				NetHeader header = NetHeader(size, NetDataType.binary);
335 			}
336 		}
337 		sendDataRaw(toNetworkBytes(toBytes(header, data)));
338 	}
339 
340 	final void withServerTarget(scope void delegate() dg)
341 	{
342 		uint currID = targetConnectionID;
343 		targetConnectionID = NetID.server;
344 		scope(exit)
345 			targetConnectionID = currID;
346 		dg();
347 	}
348 
349 
350 	void sendDataToServer(T)(T data)
351 	{
352 		withServerTarget((){sendData(data);});
353 	}
354 
355 	void disconnect();
356 	final bool isConnected() const { return status == NetConnectStatus.connected; }
357 
358 }